List table
Allow users to easily scan and quickly access key data organized in logical, hierarchical patterns
#Examples
Composition:
- Background: A subtle background color can visually separate the list table from surrounding content. Use a neutral color that doesn't compete with the content.
- Item Count (Optional): Display the total number of items in the list, especially for filtered or large datasets. Add context when applicable (e.g., "Filtered by: Active").
- Sorting Indicator (Optional): Clearly indicate the active sorting criteria (e.g., "Sorted by Name (Ascending)"). This helps users understand how the list is organized.
- Rows and Cells (Optional): Rows represent individual data items, while cells hold specific pieces of information within a row.
#Basic usage
Use Cases:
- Surface essential information quickly (e.g., site names, scores, policy summaries).
- Offer an overview of data, acting as an entry point to more details. (e.g., Core wins).
- Enable users to scan for patterns and insights.
Best Practices:
- Focus on the most important data points, keeping information density low to avoid overwhelming users.
- Consider embedding list tables within cards or other primary content areas to provide context.
- Keep additional actions (shortcuts, CTAs) to a maximum of two per row to maintain visual clarity.
Dish | Calories | Protein |
|---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
Grilled Salmon | 500 calories | 30 g protein |
Veggie Lasagne | 350 calories | 12 g protein |
<ListTable
items={sortItems(items, { property: "dish", direction: "asc" })}
columns={[
{
header: { content: "Dish" },
render: (item) => <div>{item.dish}</div>,
options: { isKeyColumn: true },
},
{ header: { content: "Calories" }, render: (item) => <div>{item.calories}</div> },
{ header: { content: "Protein" }, render: (item) => <div>{item.protein}</div> },
]}
loading={false}
caption="Some table data in a list"
{...translations}
/>#Usage with pagination
Use for large datasets. Provide clear controls and indicate the total number of pages. Consider offering options to adjust items per page.
Dish | Calories | Protein |
|---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
Grilled Salmon | 500 calories | 30 g protein |
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(2);
const visibleItems = sortItems(items, { property: "dish", direction: "asc" }).slice(
(page - 1) * pageSize,
page * pageSize
);
return (
<ListTable
items={visibleItems}
columns={[
{
header: { content: "Dish" },
render: (item) => <div>{item.dish}</div>,
options: { isKeyColumn: true },
},
{ header: { content: "Calories" }, render: (item) => <div>{item.calories}</div> },
{ header: { content: "Protein" }, render: (item) => <div>{item.protein}</div> },
]}
loading={false}
caption="Some table data in a list"
pagination={{
total: items.length,
count: pageSize,
page: page,
setPage: setPage,
pageSize: pageSize,
setPageSize: setPageSize,
cancelLabel: "Cancel",
confirmLabel: "Confirm",
firstLabel: "First",
prevLabel: "Previous",
nextLabel: "Next",
lastLabel: "Last",
pagingInfoLabel: (startIdx: number, endIdx: number, total: number) =>
`${startIdx} - ${endIdx} of ${total} items`,
pageLabel: "Page",
pageXofYLabel: (current: number, total: number) => `Page ${current} of ${total}`,
pageSizeSelectionLabel: (pageSize: number) => `${pageSize} items`,
pageSizeSelectorPrefix: "Show",
pageSizeSelectorPostfix: "per page",
pageSizeLabel: "Items per page",
defaultError: "Default pagination error",
wholeNumberError: "Must be a whole number",
outOfBoundsError: (total: number) => `Enter a number between 1 and ${total}`,
}}
{...translations}
/>
);#Usage with load more buttons
Alternative to pagination when space is limited or the total item count is unknown. Ensure the button is visible and provide loading feedback.
Dish | Calories | Protein |
|---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
<ListTable
sort={{ property: "dish", direction: "asc" }}
items={sortItems(items, { property: "dish", direction: "asc" })}
columns={[
{
header: { content: "Dish", property: "dish" },
render: (item) => <div>{item.dish}</div>,
options: { isKeyColumn: true },
},
{ header: { content: "Calories" }, render: (item) => <div>{item.calories}</div> },
{ header: { content: "Protein" }, render: (item) => <div>{item.protein}</div> },
]}
loading={false}
caption="Some table data in a list"
loadMoreCount={1}
showLoadAll
{...translations}
/>#Usage with sorting
Allow users to reorder the list. Use a Select for options and indicate the current sorting state.
When using sorted lists, you must provide the sortSelect and sort properties as well as the colums[].header.property value of sortable columns.
Dish | Calories | Protein |
|---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
Grilled Salmon | 500 calories | 30 g protein |
Veggie Lasagne | 350 calories | 12 g protein |
const [sort, setSort] = useState(sortOptions[0]);
return (
<ListTable
items={sortItems(items, sort)}
columns={[
{
header: { content: "Dish", property: "dish" },
render: (item) => <div>{item.dish}</div>,
options: { isKeyColumn: true },
},
{ header: { content: "Calories" }, render: (item) => <div>{item.calories}</div> },
{ header: { content: "Protein" }, render: (item) => <div>{item.protein}</div> },
]}
loading={false}
sort={sort}
sortSelect={sortSelectPropsHelper(
"Some a11y label",
sortOptions,
sort,
(property, direction) =>
setSort(
sortOptions.find((x) => x.property === property && x.direction === direction) ||
sortOptions[0]
)
)}
caption="Some table data in a list"
{...translations}
/>
);#Usage with table toolbar
Provide a centralized space for actions that apply to the entire table (filtering, bulk actions, customization). Keep it uncluttered and visually distinct.
Dish | Calories | Protein |
|---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
Grilled Salmon | 500 calories | 30 g protein |
Veggie Lasagne | 350 calories | 12 g protein |
type Calories = { id: number; name: string };
const caloriesAmount: Calories[] = [
{ id: 1, name: "All calories" },
{ id: 2, name: "Less than 500 calories" },
{ id: 3, name: "Less than 400 calories" },
];
const [calories, setCalories] = useState<Calories | undefined>(caloriesAmount[0]);
const onChange = (newValue: Calories | undefined) => {
console.log("Filter changed, calling API with new calories count", newValue);
setCalories(newValue);
};
const [filterButton, activeFilters] = useSingleFilter(calories, onChange, {
label: "Calories",
stringify: (calories) => calories?.name,
items: caloriesAmount.map((calories) => ({ title: calories.name, value: calories })),
compareFn: (a, b) => a.id === b.id,
defaultOption: caloriesAmount[0],
});
const sort: SortField<typeof items[0]> = {
property: "dish",
direction: "asc",
};
const displayedItems =
calories?.id === 1
? items
: items.filter((x) => {
const caloriesMatch = x.calories.match(/\d+/);
if (!caloriesMatch) return false;
const caloriesValue = Number(caloriesMatch[0]);
return calories?.id === 2 ? caloriesValue < 500 : caloriesValue < 400;
});
return (
<>
<TableToolbar filter={filterButton} activeFilters={activeFilters} />
<ListTable
items={sortItems(displayedItems, { property: sort.property, direction: sort.direction })}
columns={[
{
header: { content: "Dish", property: "dish" },
render: (displayedItems) => <div>{displayedItems.dish}</div>,
options: { isKeyColumn: true },
},
{
header: { content: "Calories" },
render: (displayedItems) => <div>{displayedItems.calories}</div>,
},
{
header: { content: "Protein" },
render: (displayedItems) => <div>{displayedItems.protein}</div>,
},
]}
sort={sort}
loading={false}
caption="Some table data in a list"
{...translations}
/>
</>
);#Usage without filter, sort or count header
For simple, static lists where these features aren't needed. Ensure the list is self-explanatory and visually organized. Consider a card header or title for context.
Dish | Calories | Protein |
|---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
Grilled Salmon | 500 calories | 30 g protein |
Veggie Lasagne | 350 calories | 12 g protein |
return (
<ListTable
items={sortItems(items, { property: "dish", direction: "asc" })}
columns={[
{
header: { content: "Dish" },
render: (item) => <div>{item.dish}</div>,
options: { isKeyColumn: true },
},
{ header: { content: "Calories" }, render: (item) => <div>{item.calories}</div> },
{ header: { content: "Protein" }, render: (item) => <div>{item.protein}</div> },
]}
loading={false}
caption="Some table data in a list"
loadMoreLabel={(count: number) => `Load ${count} more`}
loadAllLabel="Load all"
// leave out the "itemsFoundLabel", "sortByLabel" & "sortedByLabel" props to avoid the header
/>
);#Loading state
Use a Spinner and/or brief message to indicate data is being fetched.
Best Practices:
- Use a Spinner component to indicate the loading state.
- Place the loading indicator in the center of the list table area.
- Consider adding a brief message explaining the delay.
Dish | Calories | Protein |
|---|---|---|
Loading | ||
<ListTable
items={sortItems([] as typeof items, { property: "dish", direction: "asc" })}
columns={[
{
header: { content: "Dish" },
render: (item) => <div>{item.dish}</div>,
options: { isKeyColumn: true },
},
{ header: { content: "Calories" }, render: (item) => <div>{item.calories}</div> },
{ header: { content: "Protein" }, render: (item) => <div>{item.protein}</div> },
]}
loading={true}
caption="Some table data in a list"
{...translations}
/>#No data state
Communicate clearly that no data matches the criteria. Offer suggestions or actions to help the user.
Best Practices:
- Use a visually distinct "empty state" design, read more in Empty state.
- Provide a clear, concise message explaining why no data is available.
- Offer relevant actions, such as "Try a different search" or "Create a new item".
- Avoid blaming the user or using negative language.
Consideration:
The noDataState is used to indicate that no data is available for display. An Empty state component with the type "reassure" and default heading is shown, but it can be overridden by another type with custom text. For guidelines please refer to the Empty state component.
To avoid confusion, in cases where the user has resolved the issues, explain why the data cannot be displayed. E.g. "Accessibility issues have been resolved".
Dish | Calories | Protein |
|---|---|---|
No data to display | ||
<ListTable
items={sortItems([] as typeof items, { property: "dish", direction: "asc" })}
columns={[
{
header: { content: "Dish" },
render: (item) => <div>{item.dish}</div>,
options: { isKeyColumn: true },
},
{ header: { content: "Calories" }, render: (item) => <div>{item.calories}</div> },
{ header: { content: "Protein" }, render: (item) => <div>{item.protein}</div> },
]}
loading={false}
caption="Some table data in a list"
{...translations}
/>#Properties
Dish | Calories | Protein |
|---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
Grilled Salmon | 500 calories | 30 g protein |
Veggie Lasagne | 350 calories | 12 g protein |
| Property | Description | Defined | Value |
|---|---|---|---|
columnsRequired | type-union[]Column configurations | ||
itemsRequired | unknown[]Items to be displayed | ||
loadingRequired | booleanIs the table in a loading state? | ||
noDataStateOptional | elementContent to be shown when there's no data. An Empty State component is shown by default, but it can be overridden by another type with custom text. | ||
captionOptional | stringCaption for the list table | ||
withoutKeyColumnOptional | booleanDoes this table not have a key column? Be sure this is the case otherwise the table is less accessible | ||
itemsFoundLabelOptional | functionLabel describing how many items are in the table | ||
sortByLabelOptional | stringLabel describing the dropdown with sort options | ||
sortedByLabelOptional | stringLabel describing by which property the table is sorted | ||
data-observe-keyOptional | stringUnique string, used by external script e.g. for event tracking | ||
classNameOptional | stringCustom className that's applied to the outermost element (only intended for special cases) | ||
styleOptional | objectStyle object to apply custom inline styles (only intended for special cases) |
#Guidelines
#Best practices
#Do not use when
#Accessibility
Explore detailed guidelines for this component: Accessibility Specifications